package net.cyndeline.scalarlib.rldungeon.dgs.strategy.pointlessArea.algorithms

import scalax.collection.immutable.Graph
import net.cyndeline.scalarlib.rldungeon.dgs.strategy.help.{CollapsedNode, CollapsedEdge}
import net.cyndeline.scalarlib.rldungeon.dgs._
import net.cyndeline.scalarlib.rldungeon.dgs.graph.{DGSFunctionality, Room}
import com.escalatesoft.subcut.inject.{BindingModule, Injectable}
import scala.reflect.runtime.universe._
import scala.reflect._
import scala.util.Random
import scalax.collection.GraphEdge.UnDiEdge
import net.cyndeline.scalarlib.rlgraph.util.EdgeCopyFactory
import scalax.collection.GraphPredef.OuterEdge
import net.cyndeline.scalarlib.rldungeon.dgs.strategy.pointlessArea.algorithms.algorithmModules._
import net.cyndeline.scalarlib.rldungeon.dgs.strategy.pointlessArea.PointlessAreaAlgorithm

/**
 * Removes pointless areas on a map by placing activators/rewards in them, giving the player a reason to visit
 * those areas. Responders are placed on any edge belonging to the main path, or an edge found between a node on
 * the main path and a node with a reward/activator on it, to ensure that it blocks off something important.
 *
 * To ensure that the level remains beatable, a responder belonging to an activator can only be placed further
 * down the map (closer to the goal) from the main area node that the activators pointless area is connected to.
 *
 * @constructor Creates a new pointless-area remover.
 */
class PointlessAreaRemover(implicit val bindingModule: BindingModule) extends Injectable with PointlessAreaAlgorithm {

  private val algorithm = injectOptional[AlgorithmSetupI] getOrElse { new AlgorithmSetup() }
  private val postProcessing = injectOptional[AppendResultPostProcessingI] getOrElse { new AppendResultPostProcessing() }
  private val roomStatusModifier = injectOptional[RoomStatusModifierI] getOrElse { new RoomStatusModifier() }

  /**
   * Applies activators, responders and rewards to a graph to remove pointless areas. If the graph cannot contain
   * enough activators/responders to place the desired amount both on and off the main path, the main path will be filled
   * first.
   *
   * @param graph A graph representation of a map. Should have pointless data computed for all its vertices outside
   *              the main path, as well as have the pointless status set to true for all those vertices.
   * @param cycleCollapsedGraph Cycle-collapsed version of the original graph.
   * @param start Node representing the start room in the level.
   * @param goal Node representing the goal room in the level.
   * @param responderAmountMainPath The maximum amount of responders that should be placed on the main path
   *                                before the remaining areas are made meaningful using only rewards.
   * @param responderAmountPointlessAreas The maximum amount of responders that should be placed on edges belonging to
   *                                      pointless areas before the remaining areas are made meaningful using only
   *                                      rewards.
   * @param data A container with objects that are re-used when multiple graphs have pointless areas removed.
   * @return A graph with all its pointless areas made meaningful with either activators or rewards.
   */
  def apply[VType <: Room[VType]: TypeTag : ClassTag, EType[X] <: UnDiEdge[X] with DGSFunctionality[X, EType] with OuterEdge[X, EType]]
            (graph: Graph[VType, EType],
            cycleCollapsedGraph: Graph[CollapsedNode[VType], CollapsedEdge[VType]#Inner],
            start: CollapsedNode[VType],
            goal: CollapsedNode[VType],
            responderAmountMainPath: Int,
            responderAmountPointlessAreas: Int,
            data: RemoverSetupData[VType, EType]): Graph[VType, EType] = {
    val responderAppender = injectOptional[SingleResponderAppenderI] getOrElse { new SingleResponderAppender(data.random) }
    val rewardAppender = injectOptional[RewardAppenderI] getOrElse { new RewardAppender(data.random) }
    val setupData = algorithm.setup(cycleCollapsedGraph, graph, start, goal)

    val edgeMapMainPath = setupData.edgeMappingMainPath
    val edgeMapPointless = setupData.edgeMappingOutsideMainPath
    var respondersOnMainPathLeft = responderAmountMainPath
    var respondersOffMainPathLeft = responderAmountPointlessAreas
    var currentRegularGraph = graph
    var currentCollapsedGraph = setupData.trimmedCollapsedGraph
    var storage = setupData.mutableStorage

    var areaConnectionsMainPath = setupData.areaConnectionsMainPath
    var areaConnectionsOffPath = setupData.areaConnectionsOffPath

    var areaConnectionsUsed = areaConnectionsMainPath // Will be reset below

    while (respondersOnMainPathLeft > 0 || respondersOffMainPathLeft > 0) {
      var mainPathUsed = true // True if the mapping of the main path is used, otherwise false

      /* The edge map decides which set of edges (on or off the main path) that should be used for responders. */
      val edgeMap = if (respondersOnMainPathLeft > 0) {
        mainPathUsed = true
        areaConnectionsUsed = areaConnectionsMainPath
        edgeMapMainPath
      } else {
        mainPathUsed = false
        areaConnectionsUsed = areaConnectionsOffPath
        edgeMapPointless
      }

      val appendResult = responderAppender.apply(currentRegularGraph, currentCollapsedGraph, storage, areaConnectionsUsed, edgeMap, data.activatorResponderProcessor, data.edgeFactory)

      if (appendResult.isEmpty) {
        if (mainPathUsed) respondersOnMainPathLeft = 0 else respondersOffMainPathLeft = 0
      } else {
        val result = appendResult.get
        val nodeWithReward = result.updatedNode
        currentRegularGraph = result.regularGraph
        currentCollapsedGraph = result.collapsedGraph
        storage = result.storage

        val postProcessResult = postProcessing.setAsMeaningful(nodeWithReward, currentCollapsedGraph, currentRegularGraph, data.edgeFactory)
        currentCollapsedGraph = postProcessResult._1
        currentRegularGraph = postProcessResult._2
        val meaningfulNodeWithReward = postProcessResult._3

        storage = postProcessing.replaceNodeInStorage(nodeWithReward, meaningfulNodeWithReward, storage)

        if (result.updatedEdge.remainingResponderCapacity < 1) {
          if (mainPathUsed) {
            areaConnectionsMainPath = postProcessing.unregisterEdge(result.updatedEdgeStorage, storage, areaConnectionsMainPath, edgeMap)
          } else {
            areaConnectionsOffPath = postProcessing.unregisterEdge(result.updatedEdgeStorage, storage, areaConnectionsOffPath, edgeMap)
          }
        }

        if (mainPathUsed) respondersOnMainPathLeft -= 1 else respondersOffMainPathLeft -= 1
      }
    }

    var rewardResult = rewardAppender.addReward(currentCollapsedGraph, currentRegularGraph, data.edgeFactory, data.rewardProcessor)

    while(rewardResult.isDefined) {
      val result = rewardResult.get
      val postProcessResult = postProcessing.setAsMeaningful(result.updatedNodeWithReward, result.updatedCollapsedGraph, result.updatedRegularGraph, data.edgeFactory)
      currentCollapsedGraph = postProcessResult._1
      currentRegularGraph = postProcessResult._2

      rewardResult = rewardAppender.addReward(currentCollapsedGraph, currentRegularGraph, data.edgeFactory, data.rewardProcessor)
    }

    /* Since the trimmed collapsed graph can be made 100% meaningful, every room in it that intersects the original
     * graph is set as meaningful in the original.
     */
    if (responderAmountMainPath > 0 || responderAmountPointlessAreas > 0)
      roomStatusModifier.markRoomsAsMeaningful(currentRegularGraph, currentCollapsedGraph, data.edgeFactory)
    else
      currentRegularGraph

  }

}

/**
 * Container for data that remains the same during multiple removals.
 *
 * @constructor Wraps a set of parameters in a common container.
 * @param activatorResponderProcessor A factory responsible for updating rooms and corridors with activators
 *                                    and responders.
 * @param rewardProcessor Factory responsible for adding rewards to a room.
 * @param edgeFactory Copies an edge while adding new vertices to it.
 * @param random Random object used to make all random selections. Seed when recreating specific maps.
 * @tparam VType Type of vertex in the original graph and represented by the collapsed nodes.
 * @tparam EType Type of edge in the original graph.
 */
case class RemoverSetupData[VType <: Room[VType], EType[X] <: UnDiEdge[X] with DGSFunctionality[X, EType] with OuterEdge[X, EType]]
  (activatorResponderProcessor: ActivatorResponderProcessor[VType, EType],
    rewardProcessor: RewardProcessor[VType, EType],
    edgeFactory: EdgeCopyFactory[VType, EType],
    random: Random)
